Skip to content

feat: system widgets foundation + price widget#895

Open
jvsena42 wants to merge 61 commits intomasterfrom
feat/system-widgets-foundation
Open

feat: system widgets foundation + price widget#895
jvsena42 wants to merge 61 commits intomasterfrom
feat/system-widgets-foundation

Conversation

@jvsena42
Copy link
Copy Markdown
Member

@jvsena42 jvsena42 commented Apr 9, 2026

This PR:

  1. Adds a Bitcoin Price home screen widget using Jetpack Glance
  2. Implements independent widget preferences stored in a separate DataStore
  3. Uses WorkManager for periodic background data refresh (15-minute intervals)
  4. Includes a configuration Activity for widget settings on click
  5. Renders a sparkline chart via Bitmap using Android Canvas API

Description

Introduces the foundation for Android home screen widgets (AppWidgets) using Jetpack Glance, starting with the Price widget. The widget displays enabled trading pairs with price, change percentage, and a line chart — matching the in-app Price widget layout.

The architecture reuses the existing PriceService for data fetching (via a new fetchData(period) overload) while keeping widget preferences completely independent from the in-app widget system through a dedicated AppWidgetPreferencesStore backed by its own DataStore file.

A Glance design system (GlanceTextStyles, GlanceColors) ports the app's typography and color tokens to Glance equivalents, and shared components (GlanceWidgetScaffold, GlanceDataRow) provide consistent widget layouts.

Preview

Screen_recording_20260421_153032.webm

QA Notes

  1. Long-press the home screen and select "Widgets"
    • Find "Bitcoin Price" in the widget picker
    • Verify the preview shows a price layout with mock data
  2. Add the widget to the home screen
    • Verify the configuration screen opens with trading pair and period options
    • Toggle pairs and period, tap Save
    • Verify the widget updates to show selected pairs with price, change %, and sparkline chart
  3. Wait 15+ minutes or force-stop and reopen
    • Verify the widget refreshes data via WorkManager
  4. Tap the widget
    • Verify it opens the widget settings screen
    • Verify the widget update after saving new preferences
  5. Long-press the widget
    • Verify the widget is resizable
    • Verify the chat is hidden if not enough vertical space
  6. Verify the OS widget settings are independent from internal app widgets

@jvsena42 jvsena42 changed the title feat: add price home screen widget feat: System widgets foundation + price widget Apr 9, 2026
@jvsena42 jvsena42 self-assigned this Apr 10, 2026
@jvsena42 jvsena42 marked this pull request as ready for review April 21, 2026 18:18
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test body only - inline comments follow in separate calls

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test

claude[bot]

This comment was marked as resolved.

@jvsena42 jvsena42 marked this pull request as draft April 22, 2026 13:01
@jvsena42 jvsena42 marked this pull request as ready for review April 22, 2026 13:08
Comment thread app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt Outdated
Comment thread app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt Outdated
@jvsena42 jvsena42 marked this pull request as draft April 22, 2026 14:29
@jvsena42 jvsena42 marked this pull request as ready for review April 22, 2026 14:39
@jvsena42 jvsena42 requested review from ovitrif and pwltr April 22, 2026 17:07
Comment on lines +41 to +44
override suspend fun fetchData(): Result<PriceDTO> {
val period = widgetsStore.data.first().pricePreferences.period ?: GraphPeriod.ONE_DAY
return fetchData(period)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: fetchData() no-arg overload can throw instead of returning Result.failure

The refactored no-arg fetchData() overload no longer wraps widgetsStore.data.first() in runCatching. The original implementation wrapped the entire body, so DataStore IOException or CorruptionException from reading widget preferences were captured as Result.failure. Now they propagate as thrown exceptions to callers who expect only Result success/failure.

override suspend fun fetchData(): Result<PriceDTO> {
val period = widgetsStore.data.first().pricePreferences.period ?: GraphPeriod.ONE_DAY
return fetchData(period)
}

Suggested change
override suspend fun fetchData(): Result<PriceDTO> {
val period = widgetsStore.data.first().pricePreferences.period ?: GraphPeriod.ONE_DAY
return fetchData(period)
}
override suspend fun fetchData(): Result<PriceDTO> = runCatching {
val period = widgetsStore.data.first().pricePreferences.period ?: GraphPeriod.ONE_DAY
fetchData(period).getOrThrow()
}

color: ColorProvider? = null,
maxLines: Int = Int.MAX_VALUE,
) {
Text(text = text, modifier = modifier, style = GlanceTextStyles.subtitle.withColor(color), maxLines = maxLines)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLAUDE.md violation: modifier is not the last argument in composable calls

All 6 Text(...) calls in this file pass modifier as the second argument, with style and maxLines following it. Per CLAUDE.md: "ALWAYS pass modifier = ... as the LAST argument in composable calls"

The same pattern occurs on all 6 wrapper composables (lines 17, 27, 37, 47, 57, 67).

) {
Text(text = text, modifier = modifier, style = GlanceTextStyles.subtitle.withColor(color), maxLines = maxLines)
}

Suggested change
Text(text = text, modifier = modifier, style = GlanceTextStyles.subtitle.withColor(color), maxLines = maxLines)
Text(text = text, style = GlanceTextStyles.subtitle.withColor(color), maxLines = maxLines, modifier = modifier)

Apply the same reordering to BodyMSB (line 27), BodySSB (line 37), BodySB (line 47), CaptionB (line 57), and FootnoteM (line 67).

Comment on lines +120 to +125
Row(
modifier = Modifier
.padding(vertical = 21.dp, horizontal = 16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLAUDE.md violation: modifier is not the last argument in Row composable call

modifier is passed as the first argument with horizontalArrangement following it. Per CLAUDE.md: "ALWAYS pass modifier = ... as the LAST argument in composable calls"

Row(
modifier = Modifier
.padding(vertical = 21.dp, horizontal = 16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
SecondaryButton(

Suggested change
Row(
modifier = Modifier
.padding(vertical = 21.dp, horizontal = 16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.padding(vertical = 21.dp, horizontal = 16.dp)
.fillMaxWidth()
) {

Comment on lines +72 to +77
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.weight(1f)
.verticalScroll(rememberScrollState()),
) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLAUDE.md violation: trailing comma after modifier = ... at call sites

Per CLAUDE.md: "NEVER add a trailing comma to modifier = ... at call sites"

This Column call is the first occurrence. The same violation also appears at:

  • SecondaryButton (line 131)
  • PrimaryButton (line 139)
  • BodySSB (line 162)
  • Icon (line 169)

Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.weight(1f)
.verticalScroll(rememberScrollState()),
) {
VerticalSpacer(26.dp)

Suggested change
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.weight(1f)
.verticalScroll(rememberScrollState()),
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.weight(1f)
.verticalScroll(rememberScrollState())
) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant